-
Notifications
You must be signed in to change notification settings - Fork 6
fix: flatten spread elements in array and object literals #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1363,33 +1363,105 @@ impl Vm { | |
| // Objects & Arrays | ||
| Instruction::CreateArray(count) => { | ||
| self.tracker.track_allocation(&self.limits)?; | ||
| let mut arr = Vec::with_capacity(count); | ||
| let mut popped = Vec::with_capacity(count); | ||
| for _ in 0..count { | ||
| arr.push(self.pop()?); | ||
| popped.push(self.pop()?); | ||
| } | ||
| popped.reverse(); | ||
| let mut arr = Vec::with_capacity(count); | ||
| for v in popped { | ||
| match v { | ||
| // A spread element flattens its source into the array. | ||
| // Each produced element counts against the allocation | ||
| // limit: spreads amplify (`[...a, ...a]` doubles the | ||
| // payload for O(1) stack pushes), so the up-front | ||
| // check alone cannot bound the result. | ||
| Value::Spread(inner) => match *inner { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Reproduction: function id(value) { return value; }
JSON.stringify({ x: id(...[1, 2]), y: 3 });Expected: Observed at this PR head: Please either implement argument-spread expansion or reject spread arguments as unsupported. The internal marker must not escape literal construction. Please add this reproduction as a regression test. |
||
| Value::Array(items) => { | ||
| for item in items { | ||
| self.tracker.track_allocation(&self.limits)?; | ||
| arr.push(item); | ||
| } | ||
| } | ||
| Value::String(s) => { | ||
| for c in s.chars() { | ||
| self.tracker.track_allocation(&self.limits)?; | ||
| arr.push(Value::String(Arc::from(c.to_string().as_str()))); | ||
| } | ||
| } | ||
| other => { | ||
| return Err(ZapcodeError::TypeError(format!( | ||
| "{} is not iterable (cannot spread into array)", | ||
| other.type_name() | ||
| ))); | ||
| } | ||
| }, | ||
| other => arr.push(other), | ||
| } | ||
| } | ||
| arr.reverse(); | ||
| self.push(Value::Array(arr))?; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Instruction::CreateObject(count) => { | ||
| self.tracker.track_allocation(&self.limits)?; | ||
| let mut obj = IndexMap::new(); | ||
| // Pop key-value pairs (or spread values) | ||
| let mut entries = Vec::new(); | ||
|
|
||
| // Each of `count` entries is either a normal property — [key, | ||
| // value] with value on top — or a single `Value::Spread(source)`. | ||
| enum Entry { | ||
| Kv(Value, Value), | ||
| Spread(Value), | ||
| } | ||
|
|
||
| let mut entries = Vec::with_capacity(count); | ||
| for _ in 0..count { | ||
| let val = self.pop()?; | ||
| let key = self.pop()?; | ||
| entries.push((key, val)); | ||
| match self.pop()? { | ||
| Value::Spread(inner) => entries.push(Entry::Spread(*inner)), | ||
| val => { | ||
| let key = self.pop()?; | ||
| entries.push(Entry::Kv(key, val)); | ||
| } | ||
| } | ||
| } | ||
| entries.reverse(); | ||
| for (key, val) in entries { | ||
| match key { | ||
| Value::String(k) => { | ||
| obj.insert(k, val); | ||
| } | ||
| _ => { | ||
| let k: Arc<str> = Arc::from(key.to_js_string().as_str()); | ||
|
|
||
| let mut obj: IndexMap<Arc<str>, Value> = IndexMap::new(); | ||
| for entry in entries { | ||
| match entry { | ||
| Entry::Kv(key, val) => { | ||
| let k = match key { | ||
| Value::String(k) => k, | ||
| other => Arc::from(other.to_js_string().as_str()), | ||
| }; | ||
| obj.insert(k, val); | ||
| } | ||
| // Merge the source's own enumerable properties; a later | ||
| // key overrides an earlier one (keeping its position). | ||
| // Per-entry limit checks, for the same amplification | ||
| // reason as array spread above. | ||
| Entry::Spread(src) => match src { | ||
| Value::Object(map) => { | ||
| for (k, v) in map { | ||
| self.tracker.track_allocation(&self.limits)?; | ||
| obj.insert(k, v); | ||
| } | ||
| } | ||
| Value::Array(items) => { | ||
| for (i, v) in items.into_iter().enumerate() { | ||
| self.tracker.track_allocation(&self.limits)?; | ||
| obj.insert(Arc::from(i.to_string().as_str()), v); | ||
| } | ||
| } | ||
| Value::String(s) => { | ||
| for (i, c) in s.chars().enumerate() { | ||
| self.tracker.track_allocation(&self.limits)?; | ||
| obj.insert( | ||
| Arc::from(i.to_string().as_str()), | ||
| Value::String(Arc::from(c.to_string().as_str())), | ||
| ); | ||
| } | ||
| } | ||
| // {...null}, {...undefined}, {...5}: no own props — ignore. | ||
| _ => {} | ||
| }, | ||
| } | ||
| } | ||
| self.push(Value::Object(obj))?; | ||
|
|
@@ -1492,7 +1564,10 @@ impl Vm { | |
| self.push(obj)?; | ||
| } | ||
| Instruction::Spread => { | ||
| // Handled contextually in CreateArray/CreateObject | ||
| // Wrap the top value in a transient marker that CreateArray / | ||
| // CreateObject recognize and flatten. | ||
| let v = self.pop()?; | ||
| self.push(Value::Spread(Box::new(v)))?; | ||
| } | ||
| Instruction::In => { | ||
| let right = self.pop()?; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valueis serialized with serde/postcard inside persisted VM snapshots, so insertingSpreadbeforeFunctionchangesFunction's enum discriminant from tag 8 to tag 9. A snapshot created by Zapcode 1.5.3 with a closure in scope may therefore fail to resume or decode incorrectly after upgrading to this version.Please preserve the existing serialized position by moving the new variant after
Function:For example, a snapshot suspended at
await externalTool()whileconst transform = value => value + 1is in scope serializes that closure asValue::Functionusing the old tag. Same-version snapshot tests will not detect this cross-version break. Explicit snapshot format versioning would be a good follow-up, but preserving the enum order is the minimal fix required here.