@harmoniclabs/pebble 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,20 @@
1
1
  import { IRHoisted } from "../../IRNodes/IRHoisted.js";
2
+ import { IRLetted } from "../../IRNodes/IRLetted.js";
2
3
  import { IRNative } from "../../IRNodes/IRNative/index.js";
3
4
  import { _modifyChildFromTo } from "./_modifyChildFromTo.js";
4
5
  import { iterTree } from "./iterTree.js";
5
6
  export function _makeAllNegativeNativesHoisted(term) {
6
7
  iterTree(term, elem => {
7
- if (elem instanceof IRNative && !(elem.parent instanceof IRHoisted)) {
8
+ if (elem instanceof IRNative
9
+ // already wrapped in a sharing container
10
+ && !(elem.parent instanceof IRHoisted)
11
+ // A native that is the direct value of an `IRLetted` is already
12
+ // shared via the letting mechanism, and crucially `IRLetted.value`
13
+ // UNWRAPS any `IRHoisted` assigned to it (see IRLetted.set value).
14
+ // Wrapping such a native therefore never "sticks" — the wrapper is
15
+ // immediately stripped back to the bare native and we'd loop
16
+ // forever re-wrapping it. Leave it letted.
17
+ && !(elem.parent instanceof IRLetted)) {
8
18
  _modifyChildFromTo(elem.parent, elem, new IRHoisted(elem));
9
19
  return true;
10
20
  }
@@ -61,6 +61,13 @@ export function compileIRToUPLC(term, paritalOptions = defaultOptions) {
61
61
  term = replaceNativesAndReturnRoot(term);
62
62
  // re-call rewrite to optimize introduced hoisted
63
63
  term = rewriteNativesAppliedToConstantsAndReturnRoot(term);
64
+ // the rewrite above can itself introduce custom (negative-tag) natives
65
+ // (eg. `equalsInteger(x, 0)` -> `_isZero(x)`, `addInteger(x, 1)` ->
66
+ // `_increment(x)`); lower those too, otherwise they survive as bare
67
+ // IRNatives and crash the later forcing pass with
68
+ // "getNRequiredForces ... input was: -<tag>". This only surfaced in
69
+ // contracts complex enough for the rewrite to fire on shared/hoisted bodies.
70
+ term = replaceNativesAndReturnRoot(term);
64
71
  // Lower `strictIfThenElse` triple-apps to `IRCase` BEFORE
65
72
  // `replaceForcedNativesWithHoisted` would otherwise hoist
66
73
  // `(force ifThenElse)` into a shared variable that's no longer
@@ -43,6 +43,9 @@ export declare class AstCompiler extends DiagnosticEmitter {
43
43
  get rootPath(): string;
44
44
  constructor(cfg: CompilerOptions, io?: CompilerIoApi, diagnostics?: DiagnosticMessage[]);
45
45
  private _isExporting;
46
+ /** scope used to resolve field types of synthetic internal type decls while a contract body is being derived */
47
+ private _internalDeclScope;
48
+ private _internalDeclSrcUid;
46
49
  export(funcName: string, modulePath?: string): Promise<TypedProgram>;
47
50
  /**
48
51
  * Parses the entry file, wraps all top-level statements
@@ -113,7 +116,7 @@ export declare class AstCompiler extends DiagnosticEmitter {
113
116
  private _collectNamespaceDeclarations;
114
117
  private _compileNamespaceDecl;
115
118
  private _collectInterfaceDeclarations;
116
- registerInternalTypeDecl(decl: StructDecl | TypeAliasDecl): void;
119
+ registerInternalTypeDecl(decl: StructDecl | TypeAliasDecl, scope?: AstScope, srcUid?: string): void;
117
120
  private _collectTypeDeclarations;
118
121
  private _compileStructDecl;
119
122
  private _compileTypeAliasDecl;
@@ -101,6 +101,9 @@ export class AstCompiler extends DiagnosticEmitter {
101
101
  this._isExporting = false;
102
102
  }
103
103
  _isExporting;
104
+ /** scope used to resolve field types of synthetic internal type decls while a contract body is being derived */
105
+ _internalDeclScope = undefined;
106
+ _internalDeclSrcUid = "";
104
107
  async export(funcName, modulePath) {
105
108
  this._isExporting = true;
106
109
  if (typeof funcName !== "string")
@@ -357,7 +360,7 @@ export class AstCompiler extends DiagnosticEmitter {
357
360
  i--;
358
361
  continue;
359
362
  }
360
- const funcDeclContract = this._contractDeclToFuncDecl(stmt);
363
+ const funcDeclContract = this._contractDeclToFuncDecl(stmt, topLevelScope, srcUid);
361
364
  if (!funcDeclContract) {
362
365
  // remove from array so we don't process it again
363
366
  void stmts.splice(i, 1);
@@ -665,8 +668,13 @@ export class AstCompiler extends DiagnosticEmitter {
665
668
  i--;
666
669
  }
667
670
  }
668
- registerInternalTypeDecl(decl) {
669
- this._collectTypeDeclarations([decl], "", this.preludeScope, this.preludeScope);
671
+ registerInternalTypeDecl(decl,
672
+ // scope in which the decl's field types are resolved; defaults to the
673
+ // contract source scope while a contract body is being derived (so
674
+ // synthetic decls that reference user types — eg. a redeemer whose
675
+ // fields are the method params — resolve), otherwise the prelude.
676
+ scope = this._internalDeclScope ?? this.preludeScope, srcUid = this._internalDeclSrcUid) {
677
+ this._collectTypeDeclarations([decl], srcUid, scope, scope);
670
678
  }
671
679
  _collectTypeDeclarations(stmts, srcUid, topLevelScope, srcExports) {
672
680
  for (let i = 0; i < stmts.length; i++) {
@@ -990,7 +998,10 @@ export class AstCompiler extends DiagnosticEmitter {
990
998
  }
991
999
  ;
992
1000
  }
993
- _contractDeclToFuncDecl(contractDecl) {
1001
+ _contractDeclToFuncDecl(contractDecl,
1002
+ // scope where user types referenced by the contract (eg. method param
1003
+ // types used to derive the redeemer) are resolved
1004
+ userScope = this.preludeScope, srcUid = "") {
994
1005
  const funcName = getUniqueInternalName(contractDecl.name.text);
995
1006
  const paramsInternalNamesMap = new Map();
996
1007
  const funcParams = new Array(contractDecl.params.length + 1);
@@ -1015,7 +1026,21 @@ export class AstCompiler extends DiagnosticEmitter {
1015
1026
  funcParams[contractDecl.params.length] = new SimpleVarDecl(new Identifier(scriptContextName, contractDecl.name.range), new AstNamedTypeExpr(new Identifier("ScriptContext", contractDecl.name.range), [], contractDecl.name.range), undefined, // initExpr
1016
1027
  CommonFlags.Const, contractDecl.name.range);
1017
1028
  const funcSig = new AstFuncType(funcParams, new AstVoidType(contractDecl.name.range), contractDecl.name.range);
1018
- const contractBody = _deriveContractBody(this, contractDecl, paramsInternalNamesMap, scriptContextName);
1029
+ // make user types referenced by synthetic internal type decls (the
1030
+ // redeemer / datum / per-state structs derived below) resolvable in the
1031
+ // contract's source scope, instead of only the prelude.
1032
+ const prevInternalDeclScope = this._internalDeclScope;
1033
+ const prevInternalDeclSrcUid = this._internalDeclSrcUid;
1034
+ this._internalDeclScope = userScope;
1035
+ this._internalDeclSrcUid = srcUid;
1036
+ let contractBody;
1037
+ try {
1038
+ contractBody = _deriveContractBody(this, contractDecl, paramsInternalNamesMap, scriptContextName);
1039
+ }
1040
+ finally {
1041
+ this._internalDeclScope = prevInternalDeclScope;
1042
+ this._internalDeclSrcUid = prevInternalDeclSrcUid;
1043
+ }
1019
1044
  if (!contractBody)
1020
1045
  return undefined;
1021
1046
  return new FuncDecl(new FuncExpr(new Identifier(funcName, contractDecl.name.range), CommonFlags.None, [], // typeParameters
@@ -2,7 +2,8 @@ import { FuncExpr } from "../../../../ast/nodes/expr/functions/FuncExpr.js";
2
2
  import { DiagnosticCode } from "../../../../diagnostics/diagnosticMessages.generated.js";
3
3
  import { TirCallExpr } from "../../../tir/expressions/TirCallExpr.js";
4
4
  import { TirFuncT } from "../../../tir/types/TirNativeType/native/function.js";
5
- import { canAssignTo } from "../../../tir/types/utils/canAssignTo.js";
5
+ import { canAssignTo, isOptionalEncodingBridge } from "../../../tir/types/utils/canAssignTo.js";
6
+ import { TirTypeConversionExpr } from "../../../tir/expressions/TirTypeConversionExpr.js";
6
7
  import { inferTypeArgs } from "../../../tir/types/utils/inferTypeArgs.js";
7
8
  import { substituteTypeParams } from "../../../tir/types/utils/substituteTypeParams.js";
8
9
  import { TirVariableAccessExpr } from "../../../tir/expressions/TirVariableAccessExpr.js";
@@ -147,8 +148,15 @@ export function _compileCallExpr(ctx, expr, typeHint) {
147
148
  ? funcType.returnType
148
149
  : new TirFuncT(funcType.argTypes.slice(tirArgs.length), funcType.returnType);
149
150
  for (let i = 0; i < tirArgs.length && i < funcType.argTypes.length; i++) {
150
- if (!canAssignTo(tirArgs[i].type, funcType.argTypes[i]))
151
+ if (!canAssignTo(tirArgs[i].type, funcType.argTypes[i])) {
152
+ // bridge the two incompatible `Optional` encodings with a
153
+ // real conversion instead of erroring
154
+ if (isOptionalEncodingBridge(tirArgs[i].type, funcType.argTypes[i])) {
155
+ tirArgs[i] = new TirTypeConversionExpr(tirArgs[i], funcType.argTypes[i], expr.args[i].range);
156
+ continue;
157
+ }
151
158
  return ctx.error(DiagnosticCode.Type_0_is_not_assignable_to_type_1, expr.args[i].range, tirArgs[i].type.toString(), funcType.argTypes[i].toString());
159
+ }
152
160
  }
153
161
  return new TirCallExpr(callee, tirArgs, finalCallExprType, expr.range);
154
162
  }
@@ -167,8 +175,15 @@ export function _compileCallExpr(ctx, expr, typeHint) {
167
175
  const arg = args[i];
168
176
  if (!arg)
169
177
  return undefined;
170
- if (!canAssignTo(arg.type, funcType.argTypes[i]))
178
+ if (!canAssignTo(arg.type, funcType.argTypes[i])) {
179
+ // the two `Optional` encodings are incompatible but bridgeable;
180
+ // insert a real encoding conversion instead of erroring
181
+ if (isOptionalEncodingBridge(arg.type, funcType.argTypes[i])) {
182
+ args[i] = new TirTypeConversionExpr(arg, funcType.argTypes[i], expr.args[i].range);
183
+ continue;
184
+ }
171
185
  return ctx.error(DiagnosticCode.Type_0_is_not_assignable_to_type_1, expr.args[i].range, arg.type.toString(), funcType.argTypes[i].toString());
186
+ }
172
187
  }
173
188
  return new TirCallExpr(callee, args, finalCallExprType, expr.range);
174
189
  }
@@ -60,21 +60,26 @@ export function _compileCaseExprMatcher(ctx, matcher, patternType, returnTypeHin
60
60
  return ctx.error(DiagnosticCode.Type_0_is_not_assignable_to_type_1, matcher.body.range, body.type.toString(), returnTypeHint.toString());
61
61
  return new TirCaseMatcher(new TirNamedDeconstructVarDecl(memberName, new Map(), undefined, enumType, undefined, true, patternRange, ctorNameRange), body, matcher.range);
62
62
  }
63
- const pattern = _compileVarDecl(ctx, matcher.pattern, patternType);
63
+ // each arm gets its own child scope so that the pattern binders are
64
+ // scoped to the arm and do NOT leak into the enclosing block (which would
65
+ // make two mutually-exclusive arms reusing a binder name collide as
66
+ // "duplicate identifier").
67
+ const armCtx = ctx.newBranchChildScope();
68
+ const pattern = _compileVarDecl(armCtx, matcher.pattern, patternType);
64
69
  if (!pattern)
65
70
  return undefined;
66
71
  if (pattern instanceof TirSimpleVarDecl)
67
72
  return ctx.error(DiagnosticCode._case_expression_must_decontructed_the_inspected_value, matcher.pattern.range);
68
73
  if (!canAssignTo(pattern.type, patternType))
69
74
  return ctx.error(DiagnosticCode.Type_0_is_not_assignable_to_type_1, matcher.pattern.range, pattern.type.toString(), patternType.toString());
70
- let bodyCtx = ctx;
75
+ let bodyCtx = armCtx;
71
76
  if (matchedVarName && pattern instanceof TirNamedDeconstructVarDecl) {
72
77
  const parentStruct = getStructType(patternType);
73
78
  if (parentStruct) {
74
79
  const localIdx = parentStruct.constructors.findIndex(c => c.name === pattern.constrName);
75
80
  if (localIdx >= 0) {
76
81
  const parentIdx = parentStruct.parentCtorIdx(localIdx);
77
- bodyCtx = ctx.newBranchChildScope();
82
+ bodyCtx = armCtx.newBranchChildScope();
78
83
  if (parentStruct instanceof TirDataStructType
79
84
  || parentStruct instanceof TirSoPStructType) {
80
85
  bodyCtx.scope.narrowVariable(matchedVarName, parentStruct.narrowTo([parentIdx]));
@@ -148,6 +148,9 @@ function getLinearMapMethods(kT, vT) {
148
148
  lookup: new TirFuncT([kT], new TirSopOptT(vT)),
149
149
  };
150
150
  }
151
+ // Method-form signatures: the receiver (the `bytes` value the method is
152
+ // called on) is implicit and dropped from the argument list. These mirror the
153
+ // `std.bytes.*` namespace functions registered in `populateStdNamespace`.
151
154
  const bytesMethods = Object.freeze({
152
155
  length: new TirFuncT([], int_t),
153
156
  subByteString: new TirFuncT([int_t, int_t], bytes_t),
@@ -155,6 +158,14 @@ const bytesMethods = Object.freeze({
155
158
  show: new TirFuncT([], bytes_t),
156
159
  decodeUtf8: new TirFuncT([], string_t),
157
160
  prepend: new TirFuncT([int_t], bytes_t),
161
+ concat: new TirFuncT([bytes_t], bytes_t),
162
+ indexAt: new TirFuncT([int_t], int_t),
163
+ equals: new TirFuncT([bytes_t], bool_t),
164
+ lessThan: new TirFuncT([bytes_t], bool_t),
165
+ lessThanEquals: new TirFuncT([bytes_t], bool_t),
166
+ greaterThan: new TirFuncT([bytes_t], bool_t),
167
+ greaterThanEquals: new TirFuncT([bytes_t], bool_t),
168
+ toInt: new TirFuncT([], int_t),
158
169
  });
159
170
  const stringMethods = Object.freeze({
160
171
  encodeUtf8: new TirFuncT([], bytes_t),
@@ -357,7 +357,29 @@ loopReplacements, assertions = []) {
357
357
  //*/
358
358
  );
359
359
  }
360
- throw new Error("match statement with multiple non-terminating cases is not implemented yet (sorry)");
360
+ // more than one case does not terminate:
361
+ // each non-terminating case must merge its reassigned variables
362
+ // (and optional early return) into a common SoP state, then the
363
+ // rest of the body continues from that merged state.
364
+ // (mirrors the non-terminating `if` statement handling above)
365
+ // build a SoP type to return from each branch
366
+ const { sop } = getBranchStmtReturnType(reassignsAndReturns, ctx, stmt.range);
367
+ const reassignedNames = reassignsAndReturns.reassigned;
368
+ const finalExpression = new TirCaseExpr(expressifyVars(ctx, stmt.matchExpr), stmt.cases.map(_case => {
369
+ if (_case.pattern instanceof TirArrayLikeDeconstr)
370
+ throw new Error("array-like deconstruction in match statement is not supported");
371
+ _case.pattern = toNamedDeconstructVarDecl(_case.pattern);
372
+ const caseCtx = ctx.newChild();
373
+ const nestedDeconstructs = flattenSopNamedDeconstructInplace_addTopDestructToCtx_getNestedDeconstruct(_case.pattern, caseCtx);
374
+ const branchStmts = _case.body instanceof TirBlockStmt
375
+ ? _case.body.stmts
376
+ : [_case.body];
377
+ const branchBlock = new TirBlockStmt(nestedDeconstructs.concat(branchStmts), _case.body.range);
378
+ // expressify the branch so it returns the merged SoP state
379
+ const caseBody = expressifyIfBranch(caseCtx, branchBlock, reassignedNames, sop, loopReplacements);
380
+ return new TirCaseMatcher(_case.pattern, caseBody, _case.range);
381
+ }), stmt.wildcardCase ? new TirWildcardCaseMatcher(expressifyIfBranch(ctx.newChild(), stmt.wildcardCase.body, reassignedNames, sop, loopReplacements), stmt.wildcardCase.range) : undefined, sop, stmt.range);
382
+ return TirAssertAndContinueExpr.fromStmtsAndContinuation(assertions, wrapNonTerminatingFinalStmtAsCaseExpr(finalExpression, sop, ctx, stmt.range, reassignsAndReturns, bodyStmts, loopReplacements));
361
383
  }
362
384
  // else if( stmt instanceof TirForOfStmt ) {
363
385
  //
@@ -519,6 +519,31 @@ function expressifyMethodCall(ctx, methodCall) {
519
519
  if (methodName === "prepend") {
520
520
  return new TirCallExpr(TirNativeFunc.consByteString, [methodCall.args[0], objectExpr], bytes_t, exprRange);
521
521
  }
522
+ // receiver-first byte ops: `a.op( b )` -> native( a, b )
523
+ if (methodName === "concat") {
524
+ return new TirCallExpr(TirNativeFunc.appendByteString, [objectExpr, methodCall.args[0]], bytes_t, exprRange);
525
+ }
526
+ if (methodName === "indexAt") {
527
+ return new TirCallExpr(TirNativeFunc.indexByteString, [objectExpr, methodCall.args[0]], int_t, exprRange);
528
+ }
529
+ if (methodName === "equals") {
530
+ return new TirCallExpr(TirNativeFunc.equalsByteString, [objectExpr, methodCall.args[0]], bool_t, exprRange);
531
+ }
532
+ if (methodName === "lessThan") {
533
+ return new TirCallExpr(TirNativeFunc.lessThanByteString, [objectExpr, methodCall.args[0]], bool_t, exprRange);
534
+ }
535
+ if (methodName === "lessThanEquals") {
536
+ return new TirCallExpr(TirNativeFunc.lessThanEqualsByteString, [objectExpr, methodCall.args[0]], bool_t, exprRange);
537
+ }
538
+ if (methodName === "greaterThan") {
539
+ return new TirCallExpr(TirNativeFunc._gtBS, [objectExpr, methodCall.args[0]], bool_t, exprRange);
540
+ }
541
+ if (methodName === "greaterThanEquals") {
542
+ return new TirCallExpr(TirNativeFunc._gtEqBS, [objectExpr, methodCall.args[0]], bool_t, exprRange);
543
+ }
544
+ if (methodName === "toInt") {
545
+ return new TirCallExpr(TirNativeFunc._bytesToIntBE, [objectExpr], int_t, exprRange);
546
+ }
522
547
  }
523
548
  // Generic `.show()` fallback: any value whose type has a built-in
524
549
  // `_showIR` impl can be shown by emitting a TirShowExpr. User types
@@ -113,6 +113,7 @@ export declare class TirNativeFunc implements ITirExpr {
113
113
  static get _strictOr(): TirNativeFunc;
114
114
  static get _gtBS(): TirNativeFunc;
115
115
  static get _gtEqBS(): TirNativeFunc;
116
+ static get _bytesToIntBE(): TirNativeFunc;
116
117
  static get _gtInt(): TirNativeFunc;
117
118
  static get _gtEqInt(): TirNativeFunc;
118
119
  static get fstPairData(): TirNativeFunc;
@@ -709,6 +709,11 @@ export class TirNativeFunc {
709
709
  bytes_t
710
710
  ], bool_t));
711
711
  }
712
+ static get _bytesToIntBE() {
713
+ return new TirNativeFunc(IRNativeTag._bytesToIntBE, new TirFuncT([
714
+ bytes_t
715
+ ], int_t));
716
+ }
712
717
  static get _gtInt() {
713
718
  return new TirNativeFunc(IRNativeTag._gtInt, new TirFuncT([
714
719
  int_t,
@@ -15,6 +15,18 @@ export declare function getStructType(type: TirType | undefined): TirStructType
15
15
  export declare function getNamedDestructableType(type: TirType | undefined): TirNamedDestructableType | undefined;
16
16
  export declare function canAssignToOptional(type: TirType): type is TirDataOptT | TirSopOptT | TirAliasType<TirDataOptT | TirSopOptT>;
17
17
  export declare function canAssignToList(type: TirType): boolean;
18
+ /**
19
+ * The two `Optional` representations (data-encoded `TirDataOptT` and
20
+ * SoP-encoded `TirSopOptT`) are **incompatible at runtime** and therefore not
21
+ * directly assignable. They can however be bridged by a real encoding
22
+ * conversion (`TirTypeConversionExpr`, lowered via `_inlineFromData` /
23
+ * `_inlineToData`).
24
+ *
25
+ * @returns `true` if `from` and `to` are Optionals of *different* encodings
26
+ * whose type arguments are assignable — i.e. assigning `from` to `to`
27
+ * only requires inserting such an encoding conversion.
28
+ */
29
+ export declare function isOptionalEncodingBridge(from: TirType, to: TirType): boolean;
18
30
  /**
19
31
  * @returns `true` if `a` can be assigned to `b` **without** explicit cast
20
32
  *
@@ -60,6 +60,32 @@ export function canAssignToList(type) {
60
60
  // || type instanceof TirLinearMapT
61
61
  );
62
62
  }
63
+ /**
64
+ * The two `Optional` representations (data-encoded `TirDataOptT` and
65
+ * SoP-encoded `TirSopOptT`) are **incompatible at runtime** and therefore not
66
+ * directly assignable. They can however be bridged by a real encoding
67
+ * conversion (`TirTypeConversionExpr`, lowered via `_inlineFromData` /
68
+ * `_inlineToData`).
69
+ *
70
+ * @returns `true` if `from` and `to` are Optionals of *different* encodings
71
+ * whose type arguments are assignable — i.e. assigning `from` to `to`
72
+ * only requires inserting such an encoding conversion.
73
+ */
74
+ export function isOptionalEncodingBridge(from, to) {
75
+ from = getUnaliased(from);
76
+ to = getUnaliased(to);
77
+ const fromIsData = from instanceof TirDataOptT;
78
+ const fromIsSop = from instanceof TirSopOptT;
79
+ const toIsData = to instanceof TirDataOptT;
80
+ const toIsSop = to instanceof TirSopOptT;
81
+ if (!(fromIsData || fromIsSop) || !(toIsData || toIsSop))
82
+ return false;
83
+ // only bridge *across* encodings; same-encoding cases are handled by the
84
+ // normal assignability rules
85
+ if (fromIsData === toIsData)
86
+ return false;
87
+ return canAssignTo(from.typeArg, to.typeArg);
88
+ }
63
89
  /**
64
90
  * @returns `true` if `a` can be assigned to `b` **without** explicit cast
65
91
  *
@@ -53,6 +53,28 @@ interface ParseStmtOpts {
53
53
  }
54
54
  export declare class Parser extends DiagnosticEmitter {
55
55
  readonly tn: Tokenizer;
56
+ /**
57
+ * When set, the leading primary of the next parsed expression must NOT be
58
+ * interpreted as a named-object/struct literal (`Ident{ ... }`).
59
+ *
60
+ * This is needed for statements of the form `<keyword> <expr> { ... }`
61
+ * (eg. `match subject { ... }`) where the `{` introduces the statement
62
+ * block, not a struct literal body. The flag is consumed (and cleared) by
63
+ * the first `parseExprStart` call, so nested expressions still allow
64
+ * struct literals.
65
+ */
66
+ private _noStructLiteral;
67
+ /**
68
+ * When set (while parsing the subject / arm bodies of a `case` expression),
69
+ * the `is` keyword is treated as a low-precedence arm separator rather than
70
+ * the binary `is` (type-narrowing) operator. This lets an arm body absorb
71
+ * higher-precedence operators (comparisons, arithmetic, etc.) while still
72
+ * stopping before the next arm's `is`.
73
+ *
74
+ * Inside parentheses the body is re-parsed at a low precedence, so a binary
75
+ * `is` there is still recognised normally (eg. `=> ( x is Foo )`).
76
+ */
77
+ private _caseArmIsLowPrec;
56
78
  constructor(tokenizer: Tokenizer, diagnostics?: DiagnosticMessage[] | undefined);
57
79
  static parseFile(path: string, src: string, getUid?: () => string, isEntry?: boolean): [Source, DiagnosticMessage[]];
58
80
  static parseSource(src: Source, diagnostics?: DiagnosticMessage[]): DiagnosticMessage[];
@@ -142,6 +164,11 @@ export declare class Parser extends DiagnosticEmitter {
142
164
  private _parseTypeAndInitializer;
143
165
  parseTypeExpr(suppressErrors?: boolean): AstTypeExpr | undefined;
144
166
  parseParenthesizedExpr(startPos?: number | undefined): ParentesizedExpr | undefined;
167
+ /**
168
+ * Like {@link determinePrecedence} but context-sensitive: inside a `case`
169
+ * expression the `is` keyword acts as a low-precedence arm separator.
170
+ */
171
+ private precedenceOf;
145
172
  parseExpr(precedence?: Precedence): PebbleExpr | undefined;
146
173
  parseExprStart(): PebbleExpr | undefined;
147
174
  private joinPropertyCall;
@@ -81,6 +81,28 @@ import { LitContextExpr } from "../ast/nodes/expr/litteral/LitContextExpr.js";
81
81
  import { tokenIsAlsoIdentifier } from "../tokenizer/utils/tokenIsAlsoIdentifier.js";
82
82
  export class Parser extends DiagnosticEmitter {
83
83
  tn;
84
+ /**
85
+ * When set, the leading primary of the next parsed expression must NOT be
86
+ * interpreted as a named-object/struct literal (`Ident{ ... }`).
87
+ *
88
+ * This is needed for statements of the form `<keyword> <expr> { ... }`
89
+ * (eg. `match subject { ... }`) where the `{` introduces the statement
90
+ * block, not a struct literal body. The flag is consumed (and cleared) by
91
+ * the first `parseExprStart` call, so nested expressions still allow
92
+ * struct literals.
93
+ */
94
+ _noStructLiteral = false;
95
+ /**
96
+ * When set (while parsing the subject / arm bodies of a `case` expression),
97
+ * the `is` keyword is treated as a low-precedence arm separator rather than
98
+ * the binary `is` (type-narrowing) operator. This lets an arm body absorb
99
+ * higher-precedence operators (comparisons, arithmetic, etc.) while still
100
+ * stopping before the next arm's `is`.
101
+ *
102
+ * Inside parentheses the body is re-parsed at a low precedence, so a binary
103
+ * `is` there is still recognised normally (eg. `=> ( x is Foo )`).
104
+ */
105
+ _caseArmIsLowPrec = false;
84
106
  constructor(tokenizer, diagnostics = undefined) {
85
107
  super(diagnostics);
86
108
  this.tn = tokenizer;
@@ -1442,6 +1464,15 @@ export class Parser extends DiagnosticEmitter {
1442
1464
  }
1443
1465
  return new ParentesizedExpr(inner, tn.range(startPos, tn.pos));
1444
1466
  }
1467
+ /**
1468
+ * Like {@link determinePrecedence} but context-sensitive: inside a `case`
1469
+ * expression the `is` keyword acts as a low-precedence arm separator.
1470
+ */
1471
+ precedenceOf(kind) {
1472
+ if (kind === Token.Is && this._caseArmIsLowPrec)
1473
+ return Precedence.CaseExpr;
1474
+ return determinePrecedence(kind);
1475
+ }
1445
1476
  parseExpr(precedence = Precedence.Comma) {
1446
1477
  const tn = this.tn;
1447
1478
  let expr = this.parseExprStart();
@@ -1459,7 +1490,7 @@ export class Parser extends DiagnosticEmitter {
1459
1490
  // see: http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm#climbing
1460
1491
  let nextPrecedence;
1461
1492
  let prevState;
1462
- outer_while: while ((nextPrecedence = determinePrecedence(tn.peek())) >= precedence) {
1493
+ outer_while: while ((nextPrecedence = this.precedenceOf(tn.peek())) >= precedence) {
1463
1494
  prevState = tn.mark();
1464
1495
  const token = tn.next();
1465
1496
  // DO NOT DIRECTLY RETURN FROM HERE
@@ -1645,6 +1676,10 @@ export class Parser extends DiagnosticEmitter {
1645
1676
  }
1646
1677
  parseExprStart() {
1647
1678
  const tn = this.tn;
1679
+ // consume the flag: only the leading primary of this expression is
1680
+ // affected; any nested expression parsed below allows struct literals.
1681
+ const noStructLiteral = this._noStructLiteral;
1682
+ this._noStructLiteral = false;
1648
1683
  const token = tn.next(IdentifierHandling.Prefer);
1649
1684
  const startPos = tn.tokenPos;
1650
1685
  switch (token) {
@@ -1781,7 +1816,7 @@ export class Parser extends DiagnosticEmitter {
1781
1816
  const identifier = new Identifier(identifierText, tn.range(startPos, tn.pos));
1782
1817
  // LitNamedObjExpr
1783
1818
  // eg: `Identifier{ a: 1, b: 2 }`
1784
- if (tn.peek() === Token.OpenBrace) {
1819
+ if (!noStructLiteral && tn.peek() === Token.OpenBrace) {
1785
1820
  const endPos = tn.pos;
1786
1821
  const litObjExpr = this.parseExprStart();
1787
1822
  if (!(litObjExpr instanceof LitObjExpr)) {
@@ -1792,7 +1827,7 @@ export class Parser extends DiagnosticEmitter {
1792
1827
  }
1793
1828
  // LitNamedObjExpr with type qualifier
1794
1829
  // eg: `Type.Constructor{ a: 1, b: 2 }`
1795
- if (tn.peek() === Token.Dot) {
1830
+ if (!noStructLiteral && tn.peek() === Token.Dot) {
1796
1831
  const savedState = tn.mark();
1797
1832
  tn.next(); // consume '.'
1798
1833
  if (tn.skipIdentifier(IdentifierHandling.Always)) {
@@ -1902,47 +1937,53 @@ export class Parser extends DiagnosticEmitter {
1902
1937
  const tn = this.tn;
1903
1938
  // at 'case': Expression ('is' VarDecl '=>' Expression)+ ('else' Expression)?
1904
1939
  const startPos = tn.tokenPos;
1905
- // parse the matched expression at a precedence higher than `is`
1906
- // so the case-level `is` is left unconsumed
1907
- const expr = this.parseExpr(Precedence.Relational + 1);
1908
- if (!expr)
1909
- return undefined;
1910
- const cases = new Array();
1911
- while (tn.skip(Token.Is)) {
1912
- const startPos = tn.tokenPos;
1913
- const matcher = this._parseVarDecl(CommonFlags.Const);
1914
- if (!matcher)
1915
- return undefined;
1916
- // SimpleVarDecl patterns are validated by the AstCompiler, which
1917
- // has type info (enum-member promotion vs invalid catch-all).
1918
- if (matcher.initExpr || matcher.type)
1919
- return this.error(DiagnosticCode.Patterns_may_not_have_initializers_or_explicit_types, matcher.initExpr ? matcher.initExpr.range : matcher.type.range);
1920
- if (!tn.skip(Token.FatArrow))
1921
- return this.error(DiagnosticCode._0_expected, tn.range(), "=>");
1922
- // parse body at precedence higher than `is` so the next case-arm's
1923
- // `is` is left unconsumed (otherwise `is` would be greedily parsed
1924
- // as the binary `is` operator inside the body)
1925
- const body = this.parseExpr(Precedence.Relational + 1);
1926
- if (!body)
1940
+ // within the case expression, `is` is a low-precedence arm separator
1941
+ const prevCaseArmIsLowPrec = this._caseArmIsLowPrec;
1942
+ this._caseArmIsLowPrec = true;
1943
+ try {
1944
+ // parse the matched expression at a precedence higher than `is`
1945
+ // so the case-level `is` is left unconsumed
1946
+ const expr = this.parseExpr(Precedence.Relational + 1);
1947
+ if (!expr)
1927
1948
  return undefined;
1928
- cases.push(new CaseExprMatcher(matcher, body, tn.range(startPos, tn.pos)));
1949
+ const cases = new Array();
1950
+ while (tn.skip(Token.Is)) {
1951
+ const startPos = tn.tokenPos;
1952
+ const matcher = this._parseVarDecl(CommonFlags.Const);
1953
+ if (!matcher)
1954
+ return undefined;
1955
+ // SimpleVarDecl patterns are validated by the AstCompiler, which
1956
+ // has type info (enum-member promotion vs invalid catch-all).
1957
+ if (matcher.initExpr || matcher.type)
1958
+ return this.error(DiagnosticCode.Patterns_may_not_have_initializers_or_explicit_types, matcher.initExpr ? matcher.initExpr.range : matcher.type.range);
1959
+ if (!tn.skip(Token.FatArrow))
1960
+ return this.error(DiagnosticCode._0_expected, tn.range(), "=>");
1961
+ // parse the body absorbing all operators tighter than `is`; the
1962
+ // low-precedence `is` (see `_caseArmIsLowPrec`) stops the body
1963
+ // before the next case-arm's `is`
1964
+ const body = this.parseExpr(Precedence.CaseExpr + 1);
1965
+ if (!body)
1966
+ return undefined;
1967
+ cases.push(new CaseExprMatcher(matcher, body, tn.range(startPos, tn.pos)));
1968
+ }
1969
+ let wildcardCase = undefined;
1970
+ if (tn.skip(Token.Else)) {
1971
+ const wildcardStart = tn.tokenPos;
1972
+ // parse the body absorbing all operators tighter than `is`
1973
+ const body = this.parseExpr(Precedence.CaseExpr + 1);
1974
+ if (!body)
1975
+ return undefined;
1976
+ wildcardCase = new CaseWildcardMatcher(body, tn.range(wildcardStart));
1977
+ }
1978
+ tn.skip(Token.Semicolon); // if any
1979
+ const finalRange = tn.range(startPos, tn.pos);
1980
+ if (cases.length < 1)
1981
+ return this.error(DiagnosticCode.A_case_expression_must_have_at_least_one_clause, finalRange);
1982
+ return new CaseExpr(expr, cases, wildcardCase, finalRange);
1929
1983
  }
1930
- let wildcardCase = undefined;
1931
- if (tn.skip(Token.Else)) {
1932
- const wildcardStart = tn.tokenPos;
1933
- // parse body at precedence higher than `is` so the next case-arm's
1934
- // `is` is left unconsumed (otherwise `is` would be greedily parsed
1935
- // as the binary `is` operator inside the body)
1936
- const body = this.parseExpr(Precedence.Relational + 1);
1937
- if (!body)
1938
- return undefined;
1939
- wildcardCase = new CaseWildcardMatcher(body, tn.range(wildcardStart));
1984
+ finally {
1985
+ this._caseArmIsLowPrec = prevCaseArmIsLowPrec;
1940
1986
  }
1941
- tn.skip(Token.Semicolon); // if any
1942
- const finalRange = tn.range(startPos, tn.pos);
1943
- if (cases.length < 1)
1944
- return this.error(DiagnosticCode.A_case_expression_must_have_at_least_one_clause, finalRange);
1945
- return new CaseExpr(expr, cases, wildcardCase, finalRange);
1946
1987
  }
1947
1988
  parseFunctionExpr() {
1948
1989
  const tn = this.tn;
@@ -2640,7 +2681,10 @@ export class Parser extends DiagnosticEmitter {
2640
2681
  const tn = this.tn;
2641
2682
  // at 'match': Expression '{' MatchStmtCase* '}' ';'
2642
2683
  const startPos = tn.pos;
2684
+ // the `{` after the subject opens the match block, not a struct literal
2685
+ this._noStructLiteral = true;
2643
2686
  const expr = this.parseExpr();
2687
+ this._noStructLiteral = false;
2644
2688
  if (!expr)
2645
2689
  return this.error(DiagnosticCode.Expression_expected, tn.range(startPos - 5, startPos));
2646
2690
  if (!tn.skip(Token.OpenBrace))
@@ -1 +1 @@
1
- export declare const COMPILER_VERSION = "0.3.1";
1
+ export declare const COMPILER_VERSION = "0.3.2";
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated by scripts/genVersion.js. Do not edit.
2
- export const COMPILER_VERSION = "0.3.1";
2
+ export const COMPILER_VERSION = "0.3.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harmoniclabs/pebble",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "A simple, yet rock solid, functional language with an imperative bias, targeting UPLC",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -68,7 +68,7 @@
68
68
  "@harmoniclabs/buildooor": "^0.2.6",
69
69
  "@types/jest": "^28.1.4",
70
70
  "@types/node": "^18.14.6",
71
- "jest": "^29.4.3",
71
+ "jest": "^30.4.2",
72
72
  "jest-environment-jsdom": "^30.3.0",
73
73
  "tsc-alias": "^1.7.1",
74
74
  "typescript": "^4.6.3"